package com.iccaplayer.midi;

/*
 * Icca Player의 정보를 MIDI로 출력
 * Format 1의 형식으로 
 * Icca의 악기 수, 트랙(비트)수, bpm, 악기 종류, 노트 정보를 담아서 출력
 * Track 이름은 ArduIns, 작곡자 이름은 MKDS
 */

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Vector;

import com.iccaplayer.arduins.ArduIns;
import com.iccaplayer.sequencer.Sequencer;

import android.content.Context;
import android.os.Environment;
import android.util.Log;

public class IccaToMidi{
	private static final String TAG = "IccaToMidi";
	
	private static final int HEX = 16;
	private static final int HEX_POWER = (16*16);
	private static final int HEX_CUBE = (16*16*16);
	
	private static final int TOTAL_INSTRUMENTS = 16;
	
	static final int SEMIQUAVER = 4;
	static final int QUAVER = 8;
	static final int CROTCHET = 16;
	static final int MINIM = 32;
	static final int SEMIBREVE = 64;
	
	private static final String[] SCALES = new String[]{"c", "cs", "d", "ds", "e", "f", "fs", "g", "gs", "a", "as", "b"};
	
	static final int fileHeader[] = new int[]{
		0x4d, 0x54, 0x68, 0x64, 
		0x00, 0x00, 0x00, 0x06,
		0x00, 0x01, // single-track format
		0x00, 0x02, // track is 2 (primary + scoreTrack)
		0x00, 0x30, // ticks per quarter	
	};
	
	static final int trackHeader[] = new int[]{
		0x4d, 0x54, 0x72, 0x6B			
	};
	
	// Setting Track Name Event
	private int trackNameEvent[] = new int[]{
		0x00, 0xFF, 0x03, // Instrument
		0x07	// A Number of Character (3)
	};
	private int trackName[] = new int[]{
		0x41, 0x72, 0x64, 0x75, 0x49, 0x6E, 0x73 // default: ArduIns 
	};
	
	// Setting Composer Name Event
	private int composerNameEvent[] = new int[]{
		0x00, 0xFF, 0x01, // Instruction
		0x04 // A Number of Character (3)
	};
	private int composerName[] = new int[]{
		0x4D, 0x4B, 0x44, 0x53
	};
	
	// Setting Time Signatures Event
	private int timeSigEvent[] = new int[]{
		0x00, 0xFF, 0x58, // Instruction
		0x04, 0x04, // 4/4
		0x02, // denominator (2==4, because it's a power of 2)
		0x30, // ticks per click (not used)
		0x08  // 32nd notes per crotchet 
	};
	
	// Setting Key Event
	static final int keySigEvent[] = new int[]{
		0x00, 0xFF, 0x59, // Instruction
		0x02,
		0x00, // C
		0x00  // major
	};
	
	// Setting Tempo
	int tempoEvent[] = new int[]{
		0x00, 0xFF, 0x51,// Instruction
		0x03, 
		0x07, 0xA1, 0x20 // (1000000 * 60) / Tempo 0<-120 (1000000*60/value = tempo0
	};
	
	private int insInfo[] = new int[]{
			0x00,
			0xC9, // (192 + trackNum)
			0x00// Index of Instrument
	};	

	// Standard footer
	static final int footer[] = new int[]{
		0x01, 0xFF, 0x2F, 0x00
	};

	private Context ctx;
	private int trackCount, trackLength, tempo;
	private int nInsType, nDrumType;
	
	protected Vector<int[]> playEvents;
	ArduIns arduIns[] = new ArduIns[TOTAL_INSTRUMENTS];
	Sequencer sequencer[] = new Sequencer[TOTAL_INSTRUMENTS];		
	
	public IccaToMidi(Context ctx, int trackCount, int trackLength, int bpm)
	{		
		super();
		this.ctx = ctx;
		this.trackCount = trackCount + 1; // Add To Primary Track
		this.trackLength = trackLength;
		this.tempo = (1000000*60) / bpm;

		// Initialize For Counting Instrument Type and Drum Kits Type
		this.nInsType = 0;
		this.nDrumType = 0;
		
		fileHeader[10] = this.trackCount / HEX;
		fileHeader[11] = this.trackCount % HEX;
//		Log.d(TAG, "count: " + this.trackCount + " :: " + fileHeader[10] + "," + fileHeader[11]);
		
		tempoEvent[4] = tempo / HEX_POWER;
		tempoEvent[5] = (tempo % HEX_POWER) / HEX;
		tempoEvent[6] = (tempo % HEX_POWER) % HEX;
		
		playEvents = new Vector<int[]>();
	}
	
	public void combineTrack(int pos, ArduIns arduIns, Sequencer sequencer){
		this.arduIns[pos] = arduIns;
		this.sequencer[pos]	 = sequencer;
	}
	
	public String createFile(String fileName) throws IOException{
		String dirName;
		File dirPath, midiFile;
		
		dirPath = null;
		midiFile = null;
		dirName = "";
		
		String state = Environment.getExternalStorageState();
		if(state.equals(Environment.MEDIA_MOUNTED)){
			dirName = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).toString();
			dirName = dirName + "/midifiles/";
			dirPath = new File(dirName);
			if(!dirPath.isDirectory()){
				if(!dirPath.mkdirs())
					Log.e(TAG, "Making Directory Is Fail!!");
			}		
			
			midiFile = new File(dirPath, fileName + ".mid");
			FileOutputStream fos = new FileOutputStream(midiFile);
			writeFile(fos);
			fos.close();
		}
		
		return midiFile.getAbsolutePath();
	}
	
	private void writeFile(FileOutputStream stream) throws IOException{
		FileOutputStream fos;
		
		fos = stream;

		fos.write(intArrayToByteArray(fileHeader));
		writePrimaryTrack(fos);
		for(int trackPos=0; trackPos<trackCount-1; trackPos++){
			writeScoreTrack(fos, trackPos);
		}		
	}
		
	private void writePrimaryTrack(FileOutputStream stream) throws IOException{
		FileOutputStream fos;
		int primarySize, high, low;
		
		fos = stream;
		
		fos.write(intArrayToByteArray(trackHeader));
		
		primarySize = composerNameEvent.length + composerName.length
				+ timeSigEvent.length + keySigEvent.length + tempoEvent.length
				+ footer.length;
		high = primarySize / HEX_POWER;
		low = primarySize - (high * HEX_POWER);		
		fos.write ((byte) 0);
		fos.write ((byte) 0);
		fos.write ((byte) high);
		fos.write ((byte) low);

		fos.write(intArrayToByteArray(composerNameEvent));
		fos.write(intArrayToByteArray(composerName));

		fos .write (intArrayToByteArray (timeSigEvent));
		fos.write (intArrayToByteArray (keySigEvent));
		fos.write (intArrayToByteArray (tempoEvent));
		
		fos.write(intArrayToByteArray(footer));		
	}
	
	private void writeScoreTrack(FileOutputStream stream, int pos) throws IOException{
		int trackPos;
		int scoreSize, high, middle, low;
		String insType;
		FileOutputStream fos;
		
		trackPos = pos;
		insType = arduIns[trackPos].getInsType();
		fos = stream;
		
		// Setting insInfo
		if(insType.equalsIgnoreCase("Instruments")){
			// Setting Channel
			if(nInsType < 9)
				insInfo[1] = 192 + nInsType;
			else if(nInsType < 15)
				insInfo[1] = 192 + nInsType + 1;
			
			// Setting Program Number
			insInfo[2] = arduIns[trackPos].getInsProgInt();
			Log.d(TAG, "insInfo: " + nInsType + "::" + insInfo[1] + "," + insInfo[2]);
			
			nInsType++;
		}
		else if(insType.equalsIgnoreCase("Drum Kits")){
			insInfo[1] = 0xC9;
			insInfo[2] = 0x00;
			nDrumType++;
		}
		
		// Create Note
		createNote(trackPos, insType);
		
		// Write Track
		fos.write(intArrayToByteArray(trackHeader));
		
		scoreSize = trackNameEvent.length + trackName.length 
				+ insInfo.length 
				+ footer.length;	
		for(int i=0; i<playEvents.size(); i++){
			scoreSize += playEvents.elementAt(i).length;
		}
		//*************strange***************************************
		high = scoreSize / HEX_CUBE;
		middle = (scoreSize % HEX_CUBE) / HEX_POWER;
		low = (scoreSize % HEX_CUBE) % HEX_POWER;	
		//**********************************************************
		fos.write ((byte) 0);
		fos.write ((byte) high);
		fos.write ((byte) middle);
		fos.write ((byte) low);
		
		fos.write(intArrayToByteArray(trackNameEvent));
		fos.write(intArrayToByteArray(trackName));		

		fos.write (intArrayToByteArray (insInfo));		
		
		for (int i = 0; i < playEvents.size(); i++)
			fos.write (intArrayToByteArray (playEvents.elementAt(i)));
		
		fos.write (intArrayToByteArray (footer));
		playEvents.clear();
		
	}
	
	private void createNote(int pos, String type){
		int trackPos, channel;
		String insType;
		int nSamples, nBeats, samplePos;
		int count, preBeat, space, removePos;
		int[] resultKey = new int[5];
		String key, pitches, octave;
		
		trackPos = pos;
		insType = type;
		octave = null;	pitches = null;	
		channel = 0xC0;
		if(insType.equalsIgnoreCase("Instruments")){
			if(nInsType < 10)
				channel = 144 + nInsType -1;
			else if(nInsType < 15)
				channel = 144 + nInsType;
		}
		else if(insType.equalsIgnoreCase("Drum Kits")){
			channel = 0x99;
		}
		
		if(playEvents != null && sequencer[trackPos] != null){
			nSamples = sequencer[trackPos].getSamples();
			nBeats = trackLength;
			
			preBeat = 0;
			space = 0;
			for(int beatPos=0; beatPos<nBeats; beatPos++){				
				count = 0;	
				for(samplePos=0; samplePos<nSamples; samplePos++){
					if(sequencer[trackPos].getCellEnabled(samplePos, beatPos)){
						key = sequencer[trackPos].getCellKey(samplePos, beatPos);
						if(key != null){
				    		if(key.length() > 2){
				    			pitches = key.substring(0, 2);
				    			octave = key.substring(2);		
				    		}
				    		else{					
				    			pitches = key.substring(0,1);
				    			octave = key.substring(1);
				    		}	
						}
						
						Log.d(TAG, "score:" + octave +  pitches);
						resultKey[count] = 12 + 12*Integer.parseInt(octave) + pitchesToInt(pitches);
						space = beatPos - preBeat;
						if(count == 0){	
							Log.d(TAG, "space: " + space);
							noteOn(5, channel, resultKey[count], 118);
						}
						else{
							noteOn(0, channel, resultKey[count], 118);
						}
						count++;
						preBeat = beatPos;	
					}
					else{
						if(samplePos == 0){
							noteOn(5, channel, 0, 0);
							noteOff(6, channel, 0);
						}
					}					
				}
				for(int index=0; index<count; index++){
					if(index == 0){
						noteOff(6, channel, resultKey[index]);
					}
					else{
						noteOff(0, channel, resultKey[index]);
					}
				}
			}
			// Remove Space Beat in Last
			removePos = preBeat*2+2;
			for(int i=playEvents.size()-1; i>removePos; i--){
				playEvents.remove(i);
			}
		}		
	}
	
	private void noteOn(int delta, int channel, int note, int velocity){
		int[] data = new int[4];
		data[0] = delta;
		data[1] = channel;
		data[2] = note;
		data[3] = velocity;
		playEvents.add (data);
	}
	
	private void noteOff(int delta, int channel, int note){
		int[] data = new int[4];
		data[0] = delta;
		data[1] = channel;
		data[2] = note;
		data[3] = 0;
		playEvents.add (data);		
	}

	protected static byte[] intArrayToByteArray (int[] ints)
	{
		int l = ints.length;
		byte[] out = new byte[ints.length]; 
		for (int i = 0; i < l; i++) 
		{
			out[i] = (byte) ints[i];
		}
		return out;
	}
	
	public int pitchesToInt(String pitches){
		int index;
		index = -1;
		for(int pos=0; pos<12; pos++){
			if(pitches.equalsIgnoreCase(SCALES[pos]))
				index = pos;
		}
		return index;
	}
	
}